<?php

/*
Copyright 2009-2011 Sam Weiss
All Rights Reserved.

This file is part of Spark/Plug.

Spark/Plug is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

if (!defined('spark/plug'))
{
	header('HTTP/1.1 403 Forbidden');
	exit('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"><html><head><title>403 Forbidden</title></head><body><h1>Forbidden</h1><p>You don\'t have permission to access the requested resource on this server.</p></body></html>');
}

// -----------------------------------------------------------------------------

class _SparkValidator extends SparkPlug
{
	private $_fields;			// the fields to validate against, indexed by field name, with rules/filters, label, and error messages for each
	private $_validator;		// an optional object that gets early crack at validating rules
	private $_errors;			// the default error messages for the built-in validation rules

	// --------------------------------------------------------------------------

	public function __construct($fields, $validator = NULL)
	{
		parent::__construct();
		
		$this->_fields = $fields;
		$this->_validator = $validator;
		$this->_errors = self::$lang->load('SparkValidator', NULL, NULL, true, false);
	}

	// --------------------------------------------------------------------------

	public function validate(&$input, &$errors)
	{
		$errors = array();

		if (empty($input))
		{
			$errors[] = 'no data';
			return false;
		}
		
		if (empty($this->_fields))
		{
			return true;
		}

		// add missing required fields to input so they can be flagged as errors
		
		foreach ($this->_fields as $name => $field)
		{
			if (!isset($input[$name]))
			{
				if (!empty($field['rule']) && strpos($field['rule'], 'required') !== false)
				{
					$input[$name] = '';
				}
			}
		}

		// validate all input

		foreach ($input as $name => $fieldValue)
		{
			// check for unexpected input
			
			if (!isset($this->_fields[$name]))
			{
				$this->setError($name, 'Unexpected input: %s', $name, '', $errors);
				continue;
			}
			
			$field = $this->_fields[$name];
			
			// if the field has no associated rules, we are done
			
			if (empty($field['rule']))
			{
				continue;
			}
		
			// extract the field's rule list
		
			$rules = $this->extractRules($field['rule']);
			
			// if field is empty and not required, we are done
			
			if (empty($fieldValue) && !$this->isRequired($rules))
			{
				continue;
			}
			
			// check each rule
			
			foreach ($rules as $rule)
			{
				// extract parameter, if any, from rule (default to field name if no explicit parameter provided)

				$param = $name;
				if (preg_match('/^(.*?)\[(.*?)\]$/', $rule, $match))
				{
					$rule	= $match[1];

					// trim spaces to the left and right of any comma that is not preceeded by a backslash

					$param = trim(preg_replace('/\s*(?<!\\\\),\s*/', '$1,', $match[2]));
				}

				// check the rule checker "stack" for a matching rule
				//		the field's rule checker, if it exists, gets first crack
				//		the global rule checker, if it exists, gets second crack
				//		the validator object itself gets third crack
				//		finally, we look for a built-in function

				$overrideError = '';
				$method = 'validate_'.$rule;
				if ((($validator = @$field['validator']) !== NULL) && method_exists($validator, $method))
				{
					$result = $validator->$method($fieldValue, $param, $input, $overrideError);
				}
				elseif (($this->_validator !== NULL) && method_exists($this->_validator, $method))
				{
					$result = $this->_validator->$method($fieldValue, $param, $input, $overrideError);
				}
				elseif (method_exists($this, $method))
				{
					$result = $this->$method($fieldValue, $param, $input, $overrideError);
				}
				elseif (function_exists($rule))
				{
					$result = $rule($fieldValue);
				}
				else
				{
					$this->observer->notify('warning:SparkValidator:norule', $rule);
					continue;
				}

				// if the rule was a filter, update the form var with the modified data

				if ($result !== false)
				{
					if (is_string($result))
					{
						$input[$name] = $result;
					}
					elseif ($result !== true)
					{
						throw new SparkHTTPException_InternalServerError(NULL, array('reason'=>'non-string result from rule filter'));
					}
					continue;
				}
				
				// if the rule failed, look up the error on the error "stack"
				//		first look for an override error returned by a validator or rule
				//		next look for a field-specific error for the rule that was violated
				//		next check for a default error for the rule that was violated
				//		next check for a field-specific general error
				//		finally, fall back on the default general error

				if (!empty($overrideError))
				{
					$error = $overrideError;
				}
				elseif (is_array(@$field['error']))
				{
					if ((!$error = @$field['error'][$rule])
						&& (!$error = @$this->_errors[$rule])
							&& (!$error = @$field['error']['general'])
								&& (!$error = @$this->_errors['general']))
									$error = '';
				}
				else
				{
					if ((!$error = @$field['error'])
						&& (!$error = @$this->_errors[$rule])
							&& (!$error = @$this->_errors['general']))
								$error = '';
				}

				$this->setError($name, $error, @$field['label'], $param, $errors);
				continue 2;
			}
		}

		return (count($errors) === 0);
	}

	// --------------------------------------------------------------------------

	private function extractRules($rules)
	{
		// first determine if we can take the easy route (no regex rules)
	
		$hasRegEx = (strpos($rules, 'regex[') !== false);
		
		$rules = explode('|', $rules);
		
		if (!$hasRegEx)
		{
			return $rules;
		}
		
		// rejoin regex rules that were split into pieces because they contained the '|' character
		
		$result = array();
		$regex = false;
		
		foreach ($rules as $rule)
		{
			if ($regex)
			{
				$regex .= '|' . $rule;
				if (strpos($rule, $end) == strlen($rule)-2)
				{
					$result[] = $regex;
					$regex = false;
				}
				continue;
			}
		
			if (strncmp('regex', $rule, 5))
			{
				$result[] = $rule;
				continue;
			}
			
			$end = $rule[6] . ']';		// delimiter + closing bracket signifies end of regex

			if (strpos($rule, $end) == strlen($rule)-2)
			{
				$result[] = $rule;
				continue;
			}

			$regex = $rule;
		}

		return $result;
	}
	
	// --------------------------------------------------------------------------

	private function isRequired($rules)
	{
		if (in_array('required', $rules))
		{
			return true;
		}
		
		// a "matches" rule involving a non-empty field becomes required
		
		foreach ($rules as $rule)
		{
			if (!strncmp($rule, 'matches[', 8))
			{
				return true;
			}
		}
		
		return false;
	}

	// --------------------------------------------------------------------------

	private function setError($name, $error, $label, $param, &$errors)
	{
		if (!isset($errors[$name]))		// don't overwrite an existing error
		{
			$params = array_merge(array(empty($label) ? $name : $label), explode(',', $param));
			$errors[$name] = SparkLang::replaceText($error, $params);
		}
	}

	// --------------------------------------------------------------------------

	// Built-in Rules

	// --------------------------------------------------------------------------

	public static function validate_alpha($item)
	{
		return ctype_alpha($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_alphanum($item)
	{
		return ctype_alnum($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_bool($item)
	{
		switch (strtolower($item))
		{
			case '0':
			case 'n':
			case 'no':
			case 'f':
			case 'false':
			case 'off':
				return '0';

			case '1':
			case 'y':
			case 'yes':
			case 't':
			case 'true':
			case 'on':
				return '1';

			default:
				return false;
		}
	}

	// --------------------------------------------------------------------------

	public static function validate_cookie($item)
	{
		return isset($_COOKIE[$item]);
	}

	// --------------------------------------------------------------------------

	public static function validate_currency($item)
	{
		return preg_match('/^\$?([0-9]{1,3}(,?[0-9]{3})*)(\.[0-9]{2})?$/', $item) ? true : false;
	}

	// --------------------------------------------------------------------------

	public static function validate_currency_max($item, $param)
	{
		if (!self::currency($item))
		{
			return false;
		}
		if (!is_numeric($param))
		{
			return false;
		}
		$amount = str_replace(array('$', ','), '', $item);
		$max = floatval($param);
		
		return ($amount <= $max);
	}

	// --------------------------------------------------------------------------

	public static function validate_currency_min($item, $param)
	{
		if (!self::currency($item))
		{
			return false;
		}
		if (!is_numeric($param))
		{
			return false;
		}
		$amount = str_replace(array('$', ','), '', $item);
		$min = floatval($param);
		
		return ($amount >= $min);
	}

	// --------------------------------------------------------------------------

	public static function validate_currency_range($item, $param)
	{
		if (!self::currency($item))
		{
			return false;
		}
		if ($param === '')
		{
			return false;
		}
		$param = explode(',', $param);
		if (count($param) != 2)
		{
			return false;
		}
		if (!is_numeric($param[0]) || !is_numeric($param[1]))
		{
			return false;
		}
		$amount = str_replace(array('$', ','), '', $item);
		$min = floatval($param[0]);
		$max = floatval($param[1]);
		
		return ($min <= $amount) && ($amount <= $max);
	}

	// --------------------------------------------------------------------------
	
	public static function validate_date($item, $param)
	{
		if (!preg_match('/\d{4}-\d{2}-\d{2}/', $item))
		{
			return false;
		}
		
		if (!empty($param))
		{
			$date = str_replace('-', '', $item);
			$today = gmdate('Ymd');
			switch ($param)
			{
				case 'today':
					return $date === $today;
				case 'past':
					return $date < $today;
				case 'future':
					return $date > $today;
			}
		}
		
		return true;
	}
	
	// --------------------------------------------------------------------------
	
	public static function validate_datetime($item, $param)
	{
		if (!preg_match('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $item))
		{
			return false;
		}
		
		if (!empty($param))
		{
			$time = str_replace(array('-',':'), '', $item);
			$now = gmdate('YmdHis');
			switch ($param)
			{
				case 'now':
					return $time === $now;
				case 'past':
					return $time < $now;
				case 'future':
					return $time > $now;
			}
		}
		
		return true;
	}
	
	// --------------------------------------------------------------------------

	public static function validate_email($item)
	{
		return SparkUtil::valid_email($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_equal($item, $param)
	{
		return ($item === $param);
	}

	// --------------------------------------------------------------------------

	public static function validate_hex($item, $param)
	{
		if (is_numeric($param))
		{
			if (strlen($item) !== intval($param))
			{
				return false;
			}
		}

		return ctype_xdigit($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_inlist($item, $param)
	{
		if ($param === '')
		{
			return false;
		}
		
		// temporarily replace any escaped commas, so we can use simple explode function with comma separator
		
		$param = str_replace('\,', '{#}', $param);

		// separate parameters

		$param = explode(',', $param);

		// restore commas
		
		foreach (array_keys($param) as $key)
		{
			$param[$key] = str_replace('{#}', ',', $param[$key]);
		}

		if (is_array($item))
		{
			$sect = array_intersect($item, $param);
			return (count($sect) == count($item));
		}
		
		return in_array($item, $param);
	}

	// --------------------------------------------------------------------------

	public static function validate_integer($item)
	{
		return ctype_digit($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_length($item, $param)
	{
		if (!is_numeric($param))
		{
			return false;
		}
		return (strlen($item) != $param) ? false : true;
	}

	// --------------------------------------------------------------------------

	public static function validate_length_max($item, $param)
	{
		if (!is_numeric($param))
		{
			return false;
		}
		return (strlen($item) > $param) ? false : true;
	}

	// --------------------------------------------------------------------------

	public static function validate_length_min($item, $param)
	{
		if (!is_numeric($param))
		{
			return false;
		}
		return (strlen($item) < $param) ? false : true;
	}

	// --------------------------------------------------------------------------

	public static function validate_length_range($item, $param)
	{
		if ($param === '')
		{
			return false;
		}
		$param = explode(',', $param);
		if (count($param) != 2)
		{
			return false;
		}
		if (!self::numeric($param[0]) || !self::numeric($param[1]))
		{
			return false;
		}
		$length = strlen($item);
		$min = intval($param[0]);
		$max = intval($param[1]);
		
		return ($min <= $length) && ($length <= $max);
	}

	// --------------------------------------------------------------------------

	public function validate_matches($item, &$param, $input)
	{
		if (!isset($input[$param]))
		{
			$result = false;
		}
		else
		{
			$result = ($item !== $input[$param]) ? false : true;
		}
		
		$label = $this->_fields[$param]['label'];
		if (is_array($label))
		{
			$label = @$label[0];
		}

		$param = $label;
		
		return $result;
	}

	// --------------------------------------------------------------------------

	public static function validate_name($item)
	{
		return preg_match('/^[a-z0-9_-\s]+$/i', $item) ? true : false;
	}

	// --------------------------------------------------------------------------

	public static function validate_nonzero($item)
	{
		if (!is_numeric($item))
		{
			return false;
		}
		return ($item != 0);
	}

	// --------------------------------------------------------------------------

	public static function validate_notempty($item)
	{
		return !empty($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_notequal($item, $param)
	{
		return ($item !== $param);
	}

	// --------------------------------------------------------------------------

	public static function validate_numeric($item)
	{
		return is_numeric($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_password($item)
	{
		$len = strlen($item);
	
		if ($len < 8)
		{
			return false;
		}
		
		$numAlpha = 0;
		$numDigit = 0;
		$numSpecial = 0;
		
		for ($i = 0 ; $i < $len; ++$i)
		{
			$char = $item[$i];
			if (ctype_digit($char))
			{
				++$numDigit;
			}
			elseif (ctype_alpha($char))
			{
				++$numAlpha;
			}
			else
			{
				++$numSpecial;
			}
		}
	
		if ($numAlpha < 2)
		{
			return false;
		}
		
		if ($numDigit + $numSpecial < 2)
		{
			return false;
		}
		
		return true;
	}

	// --------------------------------------------------------------------------

	public static function validate_phone($item)
	{
		$item = str_replace(array('(',')','-', ' '), '', $item);
		return preg_match('/^\d{10}$/', $item) ? $item : false;
	}

	// --------------------------------------------------------------------------

	public static function validate_printable($item)
	{
		return ctype_print($item);
	}

	// --------------------------------------------------------------------------

	public static function validate_regex($item, $param)
	{
		return preg_match($param, $item) ? true : false;
	}

	// --------------------------------------------------------------------------

	public static function validate_required($item)
	{
		if (is_array($item))
		{
			if (empty($item))
			{
				return false;
			}
			if (count($item) > 1)
			{
				return true;
			}
			$item = reset($item);
		}
		return (trim($item) == '') ? false : true;
	}

	// --------------------------------------------------------------------------

	public static function validate_state($item)
	{
		static $states = array
		(
			'AL'=>'',
			'AK'=>'',
			'AZ'=>'',
			'AR'=>'',
			'CA'=>'',
			'CO'=>'',
			'CT'=>'',
			'DE'=>'',
			'DC'=>'',
			'FL'=>'',
			'GA'=>'',
			'HI'=>'',
			'ID'=>'',
			'IL'=>'',
			'IN'=>'',
			'IA'=>'',
			'KS'=>'',
			'KY'=>'',
			'LA'=>'',
			'ME'=>'',
			'MD'=>'',
			'MA'=>'',
			'MI'=>'',
			'MN'=>'',
			'MS'=>'',
			'MO'=>'',
			'MT'=>'',
			'NE'=>'',
			'NV'=>'',
			'NH'=>'',
			'NJ'=>'',
			'NM'=>'',
			'NY'=>'',
			'NC'=>'',
			'ND'=>'',
			'OH'=>'',
			'OK'=>'',
			'OR'=>'',
			'PA'=>'',
			'RI'=>'',
			'SC'=>'',
			'SD'=>'',
			'TN'=>'',
			'TX'=>'',
			'UT'=>'',
			'VT'=>'',
			'VA'=>'',
			'WA'=>'',
			'WV'=>'',
			'WI'=>'',
			'WY'=>'',
			'AS'=>'',
			'CZ'=>'',
			'GU'=>'',
			'MP'=>'',
			'PR'=>'',
			'VI'=>'',
			'AB'=>'',
			'BC'=>'',
			'MB'=>'',
			'NB'=>'',
			'NL'=>'',
			'NT'=>'',
			'NS'=>'',
			'NU'=>'',
			'ON'=>'',
			'PE'=>'',
			'QC'=>'',
			'SK'=>'',
			'YT'=>'',
		);
		
		return isset($states[strtoupper($item)]);
	}

	// --------------------------------------------------------------------------

	public static function validate_taxid($item)
	{
		$item = str_replace('-', '', $item);
		return preg_match('/^\d{9}$/', $item) ? $item : false;
	}

	// --------------------------------------------------------------------------

	public static function validate_timezone($item)
	{
		try
		{
			new DateTimeZone($item);
		}
		catch (Exception $e)
		{
			return false;
		}
		return true;
	}

	// --------------------------------------------------------------------------

	public static function validate_url($item)
	{
		return SparkUtil::valid_url($item, true);
	}

	// --------------------------------------------------------------------------

	public static function validate_username($item)
	{
		$len = strlen($item);
	
		if (($len < 6) || ($len > 15))
		{
			return false;
		}
		
		if (!ctype_alpha($item[0]))
		{
			return false;
		}
		
		return self::alphanum($item);
	}
		
	// --------------------------------------------------------------------------

	public static function validate_uuid($item)
	{
		$regEx = '/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i';
		return preg_match($regEx, $item) ? true : false;
	}

	// --------------------------------------------------------------------------

	public static function validate_visible($item)
	{
		return ctype_graph($item);
	}
	
	// --------------------------------------------------------------------------

	public static function validate_zipcode($item)
	{
		$usRegEx = '[[:digit:]]{5}(-[[:digit:]]{4})?';
		$canadianRegEx = '[[:alpha:]][[:digit:]][[:alpha:]] [[:digit:]][[:alpha:]][[:digit:]]';
		$regEx = "/^(({$usRegEx})|({$canadianRegEx}))$/";

		return preg_match($regEx, $item) ? true : false;
	}

	// --------------------------------------------------------------------------
}
